<?php
/*
 * @Author: wpfly wpfly2020@qq.com
 * @Date: 2023-06-25 18:20:16
 * @LastEditors: wpfly wpfly2020@qq.com
 * @LastEditTime: 2023-06-27 23:03:43
 * @FilePath: /wpadmin/public/clipboard.php
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */

//内容加密保存到系统临时目录。
define('CLIPBOARD_FILE', sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php_clipboard.tmp');
define('CLIPBOARD_KEY', '3189e8c1b43b4089351f868a7fab49b2a8652dfd');
define('CLIPBOARD_HISTORY', 50);

/**
 * AES加密。
 * @param string $plaintext 要加密的数据。
 * @param string $key 密码。
 * @param bool $raw 加密后的数据格式，，true：原始数据，false（默认）：base64字符串
 * @return string 加密后的数据。
 */
function aesEncrypt($plaintext, $key, $raw = false)
{
    $cipher = "aes-128-cbc";
    $ivlen = openssl_cipher_iv_length($cipher);
    $iv = openssl_random_pseudo_bytes($ivlen);
    $ciphertext_raw = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
    $hmac = hash_hmac('sha1', $iv . $ciphertext_raw, $key, true);
    return $raw ? ($hmac . $iv . $ciphertext_raw) : base64_encode($hmac . $iv . $ciphertext_raw);
}
/**
 * AES解密。
 * @param string $ciphertext 要解密的数据。
 * @param string $key 密码。
 * @param bool $raw 解密前的数据格式，true：原始数据，false（默认）：base64字符串
 * @return string 解密后的数据。
 */
function aesDecrypt($ciphertext, $key, $raw = false)
{
    $cipher = "aes-128-cbc";
    $ivlen = openssl_cipher_iv_length($cipher);
    $c = $raw ? $ciphertext : base64_decode($ciphertext);
    $hmaclen = 20;
    if (strlen($c) <= $hmaclen + $ivlen) {
        throw new \Exception('Ciphertext is cut.');
    }
    $hmac = substr($c, 0, $hmaclen);
    $iv = substr($c, $hmaclen, $ivlen);
    $ciphertext_raw = substr($c, $ivlen + $hmaclen);
    if (strcmp($hmac, hash_hmac('sha1', $iv . $ciphertext_raw, $key, true)) != 0) {
        throw new \Exception('Ciphertext is modified.');
    }
    $result = openssl_decrypt($ciphertext_raw, $cipher, $key, OPENSSL_RAW_DATA, $iv);
    if ($result === false) {
        throw new \Exception('Decryption failed.');
    }
    return $result;
}

// GET请求
if($_SERVER['REQUEST_METHOD'] === 'GET'){

    // 读取数据
    $content = '';
    if(file_exists(CLIPBOARD_FILE) && !empty($fileContent = file_get_contents(CLIPBOARD_FILE))){
        $saved = unserialize(aesDecrypt($fileContent, CLIPBOARD_KEY));
        $current = count($saved["content"]) - 1;
        $content = $saved["content"][$current];
    }

    // 输出html ︾︾︾︾︾︾︾︾ 开始
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>剪贴板</title>
<style>
    body, textarea, ul, li{       
        margin: 0px;
        padding: 0px;
        font-size: 100%;
        font: inherit;
    }
    .wrap{
        position: fixed;
        left: 0px;
        right: 0px; 
        top: 0px;
        bottom: 0px;
    }
    .content{
        position: fixed;
        left: 0px;
        right: 0px;
        top: 0px;
        bottom: 66px;
    }
    .btn_bar{
        position: fixed;
        left: 0px;
        right: 0px;
        bottom: 0px;
        height: 60px;
        width: 100%;
        overflow: hidden; 
        user-select: none;    
    }
    .btn_left, .btn_right{
        padding: 10px 0px;
        width: 50%;
        height: 40px;
        line-height: 40px;
        font-size: 20px;
        text-align: center;
        cursor: pointer;
    }
    .btn_left{
        float: left;
    }
    .btn_right{
        float: right;
    }
    .current_content{
        border: none;
        outline: 0;
        display: block;
        -moz-box-sizing: border-box;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 100%;
        resize: none;
        padding: 10px;
        word-break: break-all;
    }
    .current_content:focus{
        border: none;
        outline: 0;
    }
    .history_wrap{        
        display: none;
        background-color: #ffffff; 
    }
    .history_list{
        overflow-y: scroll;
        padding: 10px;
        list-style: none;
    }
    .history_list li{
        height: 50px;
        line-height: 50px;
        border-bottom: #eeeeee 1px solid;
        cursor: pointer;
    }
    .history_list li span{
        float: left;
        width: 120px;
        text-align: center;
    }
    .history_list li span:first-child{        
        width: 40px;
    }
    .history_list li div{
        margin-left: 166px;
        height: 50px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
</style>
</head>
 
<body>
<div class="wrap">
    <textarea id="current_content" class="content current_content" placeholder="在此处输入或粘贴文本，内容会自动保存。"><?php echo $content; ?></textarea>
    <div class="btn_bar"><span id="btn_history" class="btn_left">历史记录</span><span id="btn_empty" class="btn_right">清除</span></div>
</div>
<div id="history_wrap" class="wrap history_wrap">
    <ul id="history_list" class="content history_list">
    </ul>
    <div class="btn_bar"><span id="btn_exit" class="btn_left">返回</span><span id="btn_delete" class="btn_right">全部删除</span></div>
</div>
<script>
    function DOM(id)
    {
        return document.getElementById(id);
    }
    function ajaxRequest(data, success){
        var xhr = new XMLHttpRequest();
        xhr.open('post', '<?php echo basename(__FILE__);?>');
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhr.send(data);
        xhr.onreadystatechange = function ()
        {
            if (xhr.readyState == 4)
            {	
                if(xhr.status == 200 && xhr.getResponseHeader('content-type') === 'application/json')
                {                        
                    //console.log(xhr.responseText);
                    try {
                        var result = JSON.parse(xhr.responseText);
                    }
                    catch (err)
                    {
                        console.log("服务器异常");
                        return;
                    }
                    if(result.code === 0)
                    {
                        if(success) success(result.data);
                    }
                    else
                    {
                        console.log("业务错误");
                        console.log(result);
                    }
                }
                else
                {
                    console.log("网络异常");
                }
            }
        };
    }
    function debounce(fn, delay){
        let t = null;
        return function(){
            if(t != null)
            {
                clearTimeout(t);
            }
            t = setTimeout(() => {
                fn.call(this);
            }, delay);
        };
    }
    DOM('current_content').oninput = debounce(function(){
        ajaxRequest('action=save&content=' + encodeURIComponent(this.value));
    }, 500);
    DOM('btn_history').onclick = function(){
        ajaxRequest('action=history', function(data){
            //console.log(data);
            var ul_html = data.list.length > 0 ? '' : '暂无内容！';
            for(var item of data.list) {
                ul_html += "<li><span>" + item.index + "</span>";
                ul_html += "<span>" + item.time + "</span>";
                ul_html += "<div>" + item.content + "</div></li>";
            }
            DOM('history_list').innerHTML = ul_html;
            DOM('history_wrap').style.display = 'block';
            DOM('history_list').scrollTop = 0;
            document.title = '历史记录';
        });
    }
    DOM('btn_empty').onclick = function(){        
        ajaxRequest('action=save&content=', function(){
            DOM('current_content').value = '';
        });
    }
    DOM('btn_exit').onclick = function(){
        DOM('history_wrap').style.display = 'none';
        document.title = '剪贴板';
    }
    DOM('btn_delete').onclick = function(){
        ajaxRequest('action=delete', function(){
            DOM('history_list').innerHTML = '暂无内容！';
            DOM('current_content').value = '';
        });
    }
    DOM('history_list').addEventListener('click', function(e)
    {
        var event = e || window.event;
        var target = event.target || event.srcElement;
        var li = target.parentNode;
        if(li.nodeName.toLowerCase() == 'li')
        {
            DOM('current_content').value = li.getElementsByTagName('div')[0].innerHTML;
            DOM('history_wrap').style.display = 'none';            
            document.title = '剪贴板';
        }
    });
</script>
</body>
 
</html>
<?php
    // 输出html ︽︽︽︽︽︽︽︽ 结束
}

// POST输出工具
class MyException extends \Exception
{
    protected $json;
    public function __construct($code = 0, $msg = null, $data = null)
    {
        $this->json = [
            'code' => $code,
            'msg' => $msg,
            'data' => $data,
        ];
    }
    public function getJson()
    {
        return json_encode($this->json, JSON_UNESCAPED_UNICODE);
    }
}
function success($data = null)
{
    throw new \MyException(0, 'success', $data);
}
function failure($code = 2, $msg = null, $data = null)
{
    if($code < 2)
    {
        $code = 2;
    }
    $errorMsg = [
        'ERROR_2' => '系统异常，请稍后再试',
        'ERROR_10001' => '参数action未定义',
        'ERROR_10002' => '参数content未定义',
        'ERROR_10003' => '保存失败',
        'ERROR_10004' => '清空失败',
    ];
    if(!isset($msg))
    {
        if(isset($errorMsg["ERROR_{$code}"]))
        {
            $msg = $errorMsg["ERROR_{$code}"];
        }
        else
        {
            $msg = "错误码：{$code}";
        }
    }
    throw new \MyException($code, $msg, $data);
}
function debug_log()
{
    ob_start();
    foreach (func_get_args() as $arg) {
        var_dump($arg);
    }
    $info = ob_get_clean();
    if (empty($info)) {
        return;
    }
    $date = date('Y-m-d H:i:s');
    $mesage = "[$date]\r\n$info\r\n\r\n";
    error_log($mesage, 3, 'debug.log');
}
function clipboardSave($content = '')
{
    $result = false;
    if(!file_exists(CLIPBOARD_FILE))
    {
        @touch(CLIPBOARD_FILE);
    }
    $fp = fopen(CLIPBOARD_FILE, "r");
    if(flock($fp, LOCK_EX))
    {
        $saved = [];
        $saved["content"] = [];
        $saved["time"] = [];
        
        if(!is_null($content))
        {
            if(!empty($fileContent = file_get_contents(CLIPBOARD_FILE)))
            {
                $saved = unserialize(aesDecrypt($fileContent, CLIPBOARD_KEY));
            }
            $saved["content"][] = $content;        
            $saved["time"][] = time();
            $overflowCount = count($saved["content"]) - CLIPBOARD_HISTORY;
            if($overflowCount > 0)
            {
                $saved["content"] = array_slice($saved["content"], $overflowCount);
                $saved["time"] = array_slice($saved["time"], $overflowCount);
            }
        }
        //debug_log($saved);
        $result = file_put_contents(CLIPBOARD_FILE, aesEncrypt(serialize($saved), CLIPBOARD_KEY));
        flock($fp, LOCK_UN);
    }
    fclose($fp);
    return false !== $result;
}

// POST请求
if($_SERVER['REQUEST_METHOD'] === 'POST'){
    header("Content-type:application/json");
    try
    {
        $action = empty($_POST['action']) ? '' : $_POST['action'];
        switch($action)
        {
            case "save":
                if(isset($_POST['content']))
                {
                    clipboardSave($_POST['content']) ? success() : failure(10003);
                }
                else
                {
                    failure(10002);
                }
                break;
            case "history":
                $saved = [];
                $saved["content"] = [];
                $saved["time"] = [];             
                if(file_exists(CLIPBOARD_FILE) && !empty($fileContent = file_get_contents(CLIPBOARD_FILE))){
                    $saved = unserialize(aesDecrypt($fileContent, CLIPBOARD_KEY));
                }
                $saved["content"] = array_reverse($saved["content"]);
                $saved["time"] = array_reverse($saved["time"]);
                $result = ['list'=>[]];
                foreach($saved["content"] as $k => $v)
                {
                    $result['list'][] = [
                        'index' => $k + 1,
                        'content' => $saved["content"][$k],
                        'time' => date('m-d H:i', $saved["time"][$k]),
                    ];
                }
                success($result);
                break;
            case "delete":
                clipboardSave(null) ? success() : failure(10004);
                break;
            default:
                failure(10001);
        }
    }
    catch(\MyException $e)
    {
        echo $e->getJson();
    }
    catch(\Exception $e)
    {
        echo json_encode([
            'code' => 1,
            'msg' => $e->getMessage(),
            'data' => null,
        ], JSON_UNESCAPED_UNICODE);
    }   
}
?>